/*--------------------------------------------------------------------------------
* Copyright (c) 2019,西北农林科技大学信息学院计算机科学系
* All rights reserved.
*
* 文件名称：main.c
* 文件标识：见配置管理计划书
* 摘要：BMP图像转ASCII码的演示代码(单文件结构)。
*
* 当前版本：1.0
* 作者：耿楠
* 完成日期：2022年12月18日
*
* 取代版本：无
* 原作者：
* 完成日期：
--------------------------------------------------------------------------------*/
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<math.h>

/* 文件名称最大长度 */
#define NAME_LEN 255

/* 保存当前结构体字节对齐状态 */
#pragma pack(push)

/* 设置结构体字节对齐为单字节对齐 */
#pragma pack(1)
/* 文件头结构体定义 */
typedef struct
{
    unsigned short int type;       /* 标识                    */
    unsigned int       size;       /* 文件大小(bytes          */
    unsigned short int reserved1;  /* 保留字                  */
    unsigned short int reserved2;
    unsigned int       offset;     /* 图像数据偏移地址(bytes) */
} FILEHEADER;

/* 图像信息头结构体定义 */
typedef struct
{
    unsigned int size;             /* 信息头大小(bytes)   */
    int width, height;             /* 图像宽和高(pixels)  */
    unsigned short int planes;     /* 颜色平面            */
    unsigned short int bits;       /* 颜色深度            */
    unsigned int compression;      /* 压缩类型            */
    unsigned int imagesize;        /* 图像数据大小(bytes) */
    int xresolution, yresolution;  /* 单位长度像素数      */
    unsigned int ncolours;         /* 颜色数              */
    unsigned int importantcolours; /* 重要颜色数          */
} INFOHEADER;

/* 图像数据信息结构体定义 */
typedef struct
{
    unsigned char b; /* 蓝 */
    unsigned char g; /* 绿 */
    unsigned char r; /* 红 */
    unsigned char a; /* alpha(预留) */
} RGB;
/* 恢复结构体字节对齐状态 */
#pragma pack(pop)

/* 图像数据信息结构体定义 */
typedef struct
{
    unsigned int width;      /* 位图的宽度，以像素为单位 */
    unsigned int height;     /* 位图的高度，以像素为单位 */
    unsigned int widthbytes; /* 位图的字节宽度           */
    unsigned char *data;     /* 图像数据指针             */
} BMP_Data;

/* 转换符号表 */
/* static const unsigned char ascii_char_table1[] = */
/* { */
/*     ' ', '!', '"', '#', '$', '%', '&', '\'', '(', ')', '*', '+', ',', '-', '.', '/', */
/*     '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', '<', '=', '>', '?', */
/*     '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', */
/*     'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '[', '\\', ']', '^', '_', */
/*     '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', */
/*     'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '{', '|', '}', '~' */
/* }; */

static const unsigned char ascii_char_table2[] =
{
    '#', '@', '&', '$', '%', '|', '!', ';', ':', '\'', '`', '.', ' '
};

/* 函数类型重定义 */
typedef double (*GETBLOCKFOO)(unsigned char *, int, int);

/* 图像转字符图案 */
char *Image2AsciiArt(BMP_Data *, int, int, const unsigned char *, size_t, GETBLOCKFOO pf);

/* 函数原型 */

/* 读入图像 */
int Load_BMP(const char *file_name, BMP_Data *bmp_data);
/* 图像数据垂直翻转 */
int FlipUpDown(BMP_Data *bmp_data);
/* 读入指定像素值 */
RGB GetPixel(int x, int y, BMP_Data *bmp_data);

/* 图像区块值计算方式 */
/* 对区块数据求平均值 */
double GetAverage(unsigned char *pdata, int pixW, int pixH);
/* 取区块数据的中值 */
double GetMedian(unsigned char *pdata, int pixW, int pixH);
/* 对区块数据用LOG算子进行处理 */
double GetLoG(unsigned char *pdata, int pixW, int pixH);

/* 测试 */
int main(int argc, char *argv[])
{
    /* 命令行参数不符合要求 */
    if(argc < 4 || argc > 5)
    {
        /* 命令行参数不符合要求  */
        /* Image2AsciiArt <imgfilename> <w> <h> [FooNum] */
        return 0;
    }
    /* 区块处理函数指针数组 */
    GETBLOCKFOO pFoo[] =
    {
        GetAverage, /* 对区块数据求平均值 */
        GetMedian,  /* 取区块数据的中值 */
        GetLoG      /* 对区块数据用LOG算子进行处理 */
    };

    /* 声明图像对象 */
    BMP_Data bmp_data;
    int pixW, pixH, fooidx;

    /* 读入图像 */
    if(!Load_BMP(argv[1], &bmp_data))
    {
        fprintf(stderr, "读入原图像出错!\n");
        exit(EXIT_FAILURE);
    }

    /* 图像数据垂直翻转(BMP数据是上下颠倒的) */
    FlipUpDown(&bmp_data);

    /* 开始转换 */
    char *p_art;
    sscanf(argv[2], "%d", &pixW);
    sscanf(argv[3], "%d", &pixH);

    if(argc == 5)
    {
        if(sscanf(argv[4], "%d", &fooidx) != 1)
        {
            fooidx = 0;
        }
    }
    else
    {
        fooidx = 0;
    }

    /* 未指定查找表文件，用默认查找表(1维数组) */
    p_art = Image2AsciiArt(&bmp_data, pixW, pixH,
                           ascii_char_table2, sizeof(ascii_char_table2),
                           pFoo[fooidx]);

    FILE *fp = fopen("asciiart.txt", "w");
    if(fp == NULL)
    {
        fprintf(stderr, "无法创建输出结果文件!\n");
        exit(EXIT_FAILURE);
    }

    /* AsciiArt结果尺寸 */
    int i, j;
    int artW = bmp_data.width / pixW;
    int artH = bmp_data.height / pixH;

    /* 输出结果 */
    for(i = 0; i < artH; i ++)
    {
        for(j = 0; j < artW; j++)
        {
            putchar(p_art[j + i * artW]); /* 屏幕输出 */
            putc(p_art[j + i * artW], fp); /* 文件输出 */
        }
        printf("\n"); /* 屏幕输出 */
        putc('\n', fp); /* 文件输出 */
    }

    /* 释放内存 */
    fclose(fp);
    free(p_art);
    free(bmp_data.data);

    return 0;
}

/*---------------------------------------------------------------------------------------------
// 名称：char *Image2AsciiArt(BMP_Data *pdata, int pixW, int pixH,
//                            const unsigned  char *artTab, size_t tabsize,
//                            GETBLOCKFOO pf)
// 功能：将24位真彩色图像转换为ASCII字符艺术图像
// 算法：取出指定区块的像素，然后由pf指定的函数计算后确定要转换成的字符。
// 参数：
//       [BMP_Data *bmp_data] --- 图像数据指针
//       [int pixW] --- 需要处理成单个字符的区块宽度
//       [int pixH] --- 需要处理成单个字符的区块高度
//       [const unsigned  char *artTab] --- 转换字符查找表
//       [size_t tabsize] --- 转换字符查找表长度
//       [GETBLOCKFOO pf] --- 处理区块数据的函数指针
// 返回：[char*] --- 结果字符数组的首地址
// 作者：耿楠
// 日期：2019年1月15日
// 备注：该函数动态申请了结果字符数组，注意使用后用free()进行释放。
//-------------------------------------------------------------------------------------------*/
char *Image2AsciiArt(BMP_Data *pdata, int pixW, int pixH,
                     const unsigned char *artTab, size_t tabsize, GETBLOCKFOO pf)
{
    int h, w, y;

    /* 区块总像素个数 */
    int pixsize = pixW * pixH;

    /* AsciiArt结果尺寸 */
    int artW = pdata->width / pixW;
    int artH = pdata->height / pixH;
    size_t artsize = artW * artH;

    /* 申请保存AsciiArt结果的内存空间 */
    char *p_art = malloc(artsize * sizeof(char));
    if(p_art == NULL)
    {
        fprintf(stderr, "内存不足!\n");
        exit(EXIT_FAILURE);
    }
    /* 初始化 */
    memset(p_art, 0, artsize * sizeof(char));

    /* 申请保存图像区块数据的内存空间 */
    unsigned char *p_block = malloc(pixsize * 3 * sizeof(unsigned char));
    if(p_block == NULL)
    {
        fprintf(stderr, "内存不足!\n");
        exit(EXIT_FAILURE);
    }
    /* 初始化 */
    memset(p_block, 0, pixsize * 3 * sizeof(unsigned char));

    /* 图像数据处理 */
    for(h = 0; h < artH; h++)
    {
        /* 区块起始行 */
        int startY = h * pixH;
        /* 区块起始行地址 */
        unsigned char *ptemp = pdata->data + startY * pdata->widthbytes;
        for(w = 0; w < artW; w++)
        {
            /* 区块起始列 */
            int startX = w * pixW;
            /* 定位到行首 */
            unsigned char *q = ptemp;
            /* 定位到区块起始列 */
            q += startX * 3;

            /* 读取区块数据 */
            unsigned char *pb = p_block;
            for(y = 0; y < pixH; y++)
            {
                /* 区块当前行首地址 */
                q += y * pdata->widthbytes;
                /* 复制一行数据 */
                memcpy(pb, q, pixW * 3);
                /* 区块数据定位 */
                pb += (pixW * 3);
            }
            /* 计算区块灰度平均值作为字符查找表下标 */
            double level = pf(p_block, pixW, pixH);
            int idx = (int)((tabsize - 1) * level + 0.5);
            char *p = p_art + w + h * artW;
            /* 为结果字符串数组赋值 */
            *p = artTab[idx];
        }
    }

    /* 释放内存 */
    free(p_block);

    return p_art;
}

/*-------------------------------------------------------------------------
// 名称：int Load_BMP(const char *file_name, BMP_Data *bmp_data)
// 功能：读入24位真彩色图像
// 参数：
//       [const char *file_name] --- BMP图像文件名
//       [BMP_Data *bmp_data] --- 图像数据指针
// 返回：[int] --- 成功返回1，失败返回0
// 作者：耿楠
// 日期：2010年12月30日
//-----------------------------------------------------------------------*/
int Load_BMP(const char *file_name, BMP_Data *bmp_data)
{
    /* 变量声明 */
    FILEHEADER fHeader;
    INFOHEADER bmpHeader;
    FILE *bmp_file;

    /* 打开图像文件 */
    if(!(bmp_file = fopen(file_name, "rb")))
    {
        return 0;
    }

    /* 读入文件头数据 */
    if(sizeof(fHeader) == 14)
    {
        fread( &fHeader, sizeof(fHeader), 1, bmp_file);
    }
    else
    {
        fread( &fHeader.type,      2, 1, bmp_file);
        fread( &fHeader.size,      4, 1, bmp_file);
        fread( &fHeader.reserved1, 2, 1, bmp_file);
        fread( &fHeader.reserved2, 2, 1, bmp_file);
        fread( &fHeader.offset,    4, 1, bmp_file);
    }

    /* 读入图像头数据 */
    if(sizeof(bmpHeader) == 40)
    {
        fread( &bmpHeader, sizeof(bmpHeader), 1, bmp_file);
    }
    else
    {
        fread( &bmpHeader.size,             4, 1, bmp_file);
        fread( &bmpHeader.width,            4, 1, bmp_file);
        fread( &bmpHeader.height,           4, 1, bmp_file);
        fread( &bmpHeader.planes,           2, 1, bmp_file);
        fread( &bmpHeader.bits,             2, 1, bmp_file);
        fread( &bmpHeader.compression,      4, 1, bmp_file);
        fread( &bmpHeader.imagesize,        4, 1, bmp_file);
        fread( &bmpHeader.xresolution,      4, 1, bmp_file);
        fread( &bmpHeader.yresolution,      4, 1, bmp_file);
        fread( &bmpHeader.ncolours,         4, 1, bmp_file);
        fread( &bmpHeader.importantcolours, 4, 1, bmp_file);
    }

    /* 仅处理24位BMP图像 */
    if( *((char *)&fHeader.type) != 'B' &&
        *(((char *)&fHeader.type) + 1) != 'M' &&
        bmpHeader.bits != 24 )
    {
        fclose( bmp_file );
        return 0;
    }

    /* 图像宽度和高度(像素) */
    bmp_data->width = bmpHeader.width;
    bmp_data->height = bmpHeader.height;

    /* 实际每行存储字节数 */
    bmp_data->widthbytes = ((bmp_data->width * 3) % 4 == 0) ?
                           (bmp_data->width * 3) : ((bmp_data->width * 3) / 4 * 4 + 4);

    /* 位图数据大小(字节) */
    unsigned int size = bmp_data->widthbytes * bmpHeader.height * 3;

    /* 为存储图像数据分配内存 */
    bmp_data->data = (unsigned char *)malloc( size );

    /* 调整文件指针到图像数据区 */
    fseek( bmp_file, fHeader.offset, SEEK_SET);
    fread( bmp_data->data, size, 1, bmp_file);

    /* 关闭图像文件 */
    fclose( bmp_file );

    return 1;
}

/*---------------------------------------------------------------------------
// 名称：int FlipUpDown(BMP_Data *bmp_data)
// 功能：图像数据垂直翻转
// 参数：
//       [BMP_Data *bmp_data] --- 图像数据指针
// 返回：[int] --- 成功返回1，失败返回0
// 作者：耿楠
// 日期：2019年1月16日
//-----------------------------------------------------------------------*/
int FlipUpDown(BMP_Data *bmp_data)
{
    int i;

    /* 每一行占用的字节数 */
    size_t colsize = bmp_data->widthbytes * sizeof(unsigned char);

    /* 申请存储一行数据的内存空间 */
    unsigned char *p = malloc(colsize);
    if(p == NULL)
    {
        printf("Not enough memory!\n");
        return 0;
    }
    /* 初始化 */
    memset(p, 0, colsize);

    /* 图像数据指针 */
    unsigned char *q = bmp_data->data;
    /* 参数预置 */
    unsigned int hf = bmp_data->height / 2;
    unsigned int hb = bmp_data->height - 1;

    /* 图像数据高度方向对调 */
    for(i = 0; i < hf; i++)
    {
        memcpy(p, q + i * bmp_data->widthbytes, colsize);
        memcpy(q + i * bmp_data->widthbytes, q + (hb - i) * bmp_data->widthbytes, colsize);
        memcpy(q + (hb - i) * bmp_data->widthbytes, p, colsize);
    }

    /* 释放内存 */
    free(p);

    return 1;
}

/*--------------------------------------------------------------
// 名称：RGB GetPixel(int x, int y, BMP_Data *bmp_data)
// 功能：读取图像指定位置的像素值
// 参数：
//       [int x] --- 像素x坐标
//       [int y] --- 像素y坐标(未考虑倒序)
//       [BMP_Data *bmp_data] --- 图像数据
// 返回：[RGB] --- 像素值
// 作者：耿楠
// 日期：2010年12月30日
//-----------------------------------------------------------------------*/
RGB GetPixel(int x, int y, BMP_Data *bmp_data)
{
    RGB temprgb;
    unsigned char *p;

    /* 图像数据区首指针 */
    p = bmp_data->data;
    /* 偏移至第y行 */
    p += y * bmp_data->widthbytes;
    /* 偏移至第x列 */
    p += x * 3;

    /* 读像素值(三个字节) */
    memcpy(&temprgb, p, 3);

    return temprgb;
}

/*--------------------------------------------------------------------------
// 名称：double GetAverage(unsigned char *pdata, int pixW, int pixH)
// 功能：获取区块数据的平均值
// 参数：
//       [unsigned char *pdata] --- 区块数据指针
//       [int pixW] --- 需要处理成单个字符的区块宽度
//       [int pixH] --- 需要处理成单个字符的区块高度
// 返回：无
// 作者：耿楠
// 日期：2019年1月16日
//------------------------------------------------------------------------*/
double GetAverage(unsigned char *pdata, int pixW, int pixH)
{
    int i;

    size_t size = pixW * pixH;
    double sum = 0.0;
    double pixlevel = 0.0;

    unsigned char *p = pdata;

    for(i = 0; i < size; i++)
    {
        /* 计算一个像素的归一化灰度值(0.0-1.0)，注意颜色排列为：B、G、R */
        pixlevel = 0.11 * p[2] / 255 + 0.6 * p[1] / 255 + 0.29 * p[0] / 255;
        sum += pixlevel;
        p += 3;
    }

    return sum / size;
}

/*-----------------------------------------------------------------------------
// 名称：int cmp(const void *a, const void *b)
// 功能：用于浮点数的比较大于函数
// 参数：
//       [const void *a] --- 第一个元素的起始地址
//       [const void *b] --- 第二个元素的起始地址
// 返回：[int] --- *a > *b返回1，否则返回0
// 注意：需要在函数内部进行强制地址类型转换，以实现浮点数比较
// 作者：耿楠
// 日期：2019年1月16日
// 备注：这是一个辅助函数，是GetMedian函数中的qsort函数的比较函数。
//----------------------------------------------------------------------------*/
int cmp(const void *a, const void *b)
{
    return ((*(double*)a - * (double*)b > 0) ? 1 : -1);
}

/*--------------------------------------------------------------------------
// 名称：double GetMedian(unsigned char *pdata, int pixW, int pixH)
// 功能：获取区块数据的中值
// 参数：
//       [unsigned char *pdata] --- 区块数据指针
//       [int pixW] --- 需要处理成单个字符的区块宽度
//       [int pixH] --- 需要处理成单个字符的区块高度
// 返回：无
// 作者：耿楠
// 日期：2019年1月16日
//------------------------------------------------------------------------*/
double GetMedian(unsigned char *pdata, int pixW, int pixH)
{
    int i;

    size_t size = pixW * pixH;
    double result = 0.0;

    double *plev = malloc(size * sizeof(double));
    if(plev == NULL)
    {
        printf("Not enough memory!\n");
        return 0.0;
    }
    memset(plev, 0, size * sizeof(double));

    double div = 255.0 * 3.0;

    unsigned char *p = pdata;
    for(i = 0; i < size; i++)
    {
        /* 计算一个像素的归一化灰度值(0.0-1.0)，注意颜色排列为：B、G、R */
        plev[i] = (p[2]  + p[1] + p[0]) / div;
        p += 3;
    }

    qsort(plev, size, sizeof(double), cmp);
    result = plev[size / 2];

    /* 释放内存 */
    free(plev);

    return result;
}

/*--------------------------------------------------------------------------
// 名称：double * GetLoGKer(double d, int m, int n)
// 功能：生成LOG算子
// 参数：
//       [double d] --- 方差
//       [int m] --- LOG算子高度
//       [int n] --- LOG算子宽度
// 返回：[double * ] --- LOG算子首地址
// 作者：耿楠
// 日期：2019年1月16日
// 备注：该函数将动态生成数组，调用结束后需要释放内存空间，
//          另，该函数是一个辅助函数。
//------------------------------------------------------------------------*/
double * GetLoGKer(double d, int m, int n)
{
    int i, j;

    /* 申请内存 */
    double *p = malloc(m * n * sizeof(double));
    if(p == NULL)
    {
        fprintf(stderr, "内存不足!\n");
        exit(EXIT_FAILURE);
    }
    /* 初始化 */
    memset(p, 0, m * n * sizeof(double));

    int x = (n - 1) / 2;
    int y = (m - 1) / 2;

    for(i = 0; i < n; i++)
    {
        for(j = 0; j < m; j++)
        {
            p[i + j * n] = exp(-((x - i) * (x - i) + (y - j) * (y - j) / (2 * d * d))) *
                           ((x - i) * (x - i) + (y - j) * (y - j) - 2 * d * d) / (d * d * d * d);
        }
    }

    double min = p[0];
    for(i = 0; i < m * n; i++)
    {
        if(p[i] < min)
        {
            min = p[i];
        }
    }

    for(i = 0; i < m * n; i++)
    {
        p[i] += min;
    }

    double sum = 0.0;
    for(i = 0; i < m * n; i++)
    {
        sum += p[i];
    }

    for(i = 0; i < m * n; i++)
    {
        p[i] /= sum;
    }

    return p;
}

/*--------------------------------------------------------------------------
// 名称：double GetLoG(unsigned char *pdata, int pixW, int pixH)
// 功能：获取区块数据的中值
// 参数：
//       [unsigned char *pdata] --- 区块数据指针
//       [int pixH] --- 需要处理成单个字符的区块高度
//       [int pixW] --- 需要处理成单个字符的区块宽度
// 返回：无
// 作者：耿楠
// 日期：2019年1月16日
//------------------------------------------------------------------------*/
double GetLoG(unsigned char *pdata, int pixW, int pixH)
{
    int i;

    size_t size = pixW * pixH;
    double result = 0.0;
    double pixlevel = 0.0;

    /* 生成LoG算子 */
    double *pLoG = GetLoGKer(1, pixH, pixW);

    unsigned char *p = pdata;
    for(i = 0; i < size; i++)
    {
        /* 计算一个像素的归一化灰度值(0.0-1.0)，注意颜色排列为：B、G、R */
        pixlevel = 0.11 * p[2] / 255 + 0.6 * p[1] / 255 + 0.29 * p[0] / 255;
        result += pixlevel * pLoG[i];
        p += 3;
    }

    /* 释放内存 */
    free(pLoG);

    return result;
}
